﻿using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using System.Collections;
using Microsoft.Office.Server.Diagnostics;
using System.Collections.Specialized;
using System.Text.RegularExpressions;
using System.Xml;
using Microsoft.SharePoint.WebPartPages;
using Microsoft.SharePoint.Publishing.WebControls;
using Microsoft.SharePoint.Publishing.Fields;
using Microsoft.SharePoint.Publishing;
using System.IO;

namespace SharePoint.Intranet.LinkChecker.Functions
{
    public class SiteLinkChecker
    {

        private Hashtable CheckedLinks;
        private XmlTextWriter BrokenLinkTempFile;
        private string TempFilePath;
        private Uri CurrentUri;
        private bool CurrentUriHasBrokenLinks;

        public SiteLinkChecker()
        {
            CheckedLinks = new Hashtable();
            TempFilePath = Path.GetTempFileName();
            BrokenLinkTempFile = new XmlTextWriter(TempFilePath, Encoding.Unicode);
            BrokenLinkTempFile.Formatting = Formatting.Indented;
            BrokenLinkTempFile.WriteStartElement("Pages");
            
        }

        /// <summary>
        /// Returns an XML formatted report of the broken links that have been found in scans
        /// </summary>
        /// <returns>An XmlDocument containing the report</returns>
        public string GetBrokenLinkReportPath()
        {
            BrokenLinkTempFile.WriteEndElement();
            BrokenLinkTempFile.Flush();
            BrokenLinkTempFile.Close();

            return TempFilePath;
        }

        /// <summary>
        /// Scans an entire site collection for broken links
        /// </summary>
        /// <param name="SiteToCheck">The site collection to scan for broken links</param>
        public void CheckLinksInSite(SPSite SiteToCheck)
        {
            DateTime MaximumRunTime = DateTime.Now.AddMinutes(45);
            Uri BaseUri = new Uri(SiteToCheck.Url);
            foreach (SPWeb web in SiteToCheck.AllWebs)
            {
                if (DateTime.Compare(DateTime.Now, MaximumRunTime) < 0)
                {
                    CheckLinksInWeb(web, BaseUri);
                    web.Dispose();
                }
                else
                {
                    break;
                }                
            }
        }

        /// <summary>
        /// Checks through a specific site to find broken links
        /// </summary>
        /// <param name="WebToCheck">The site to scan for broken links</param>
        /// <param name="BaseUri">The base url of the site collection that the site is found in</param>
        public void CheckLinksInWeb(SPWeb WebToCheck, Uri BaseUri)
        {
            // Check lists
            CheckListLinks(WebToCheck.Lists, BaseUri);

            //TODO: Check navigation links

            //TODO: Check links in web part pages

        }

        /// <summary>
        /// Checks through a collection of SharePoint lists to find broken links within the list content
        /// </summary>
        /// <param name="ListsToCheck">The collection of lists to check for broken links</param>
        /// <param name="BaseUri">The base url of the site collection that the lists are found in</param>
        public void CheckListLinks(SPListCollection ListsToCheck, Uri BaseUri)
        {
            foreach (SPList list in ListsToCheck)
            {

                SPListItemCollection items = list.Items;
                switch (list.BaseTemplate.ToString())
                {
                    case "850": // Pages library

                        foreach (SPListItem page in items)
                        {
                            
                            Uri PageUri = new Uri(BaseUri, page.File.ServerRelativeUrl);
                            StartNewUri(PageUri);

                            ScanListFields(page, PageUri);
                            ScanWebParts(page.File, PageUri);
                            
                            FinishCurrentUri();
                        }
                        break;

                    default:
                        foreach (SPListItem item in items)
                        {
                            Uri PageUri = new Uri(BaseUri, item.Url);
                            StartNewUri(PageUri);

                            ScanListFields(item, PageUri);

                            FinishCurrentUri();
                        }
                        break;
                }
            }
        }

        /// <summary>
        /// Scans through the fields in the list to check supported fields for broken links
        /// </summary>
        /// <param name="Item">The list item to check</param>
        /// <param name="PageUri">The full url of the page</param>
        private void ScanListFields(SPListItem Item, Uri PageUri)
        {
            foreach (SPField field in Item.Fields)
            {
                switch (field.TypeAsString)
                {
                    case "HTML":
                        try
                        {
                            if ((Item[field.Title] != null) && !string.IsNullOrEmpty(Item[field.Title].ToString()))
                            {
                                StringCollection AllLinks = LinkCheckerUtilities.GetLinksFromHTML(Item[field.Title].ToString());

                                // Make all the links relative then validate
                                foreach (string Url in AllLinks)
                                {
                                    try
                                    {
                                        ValidateLink(PageUri, new Uri(PageUri, Url), BrokenLinkType.HtmlField);
                                    }
                                    catch(Exception ex)
                                    {
                                        PortalLog.LogString("LinkChecker: Error checking link '{0}' at '{1}'", Url, PageUri.ToString());
                                    }
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            PortalLog.LogString("LinkChecker: Error: Unable to check links for field {0} at {1} - Error Message: {2}, Stack Trace: {3}", field.Title, Item.Url, ex.Message, ex.StackTrace);
                        }
                        break;
                    case "Note":
                        SPFieldMultiLineText fieldAsMultiLineText = (SPFieldMultiLineText)field;
                        if ((fieldAsMultiLineText.RichText) && (Item[field.Title] != null) && !string.IsNullOrEmpty(Item[field.Title].ToString()))
                        {
                            StringCollection AllLinks = LinkCheckerUtilities.GetLinksFromHTML(Item[field.Title].ToString());

                            foreach (string Url in AllLinks)
                            {
                                try
                                {
                                    ValidateLink(PageUri, new Uri(PageUri, Url), BrokenLinkType.HtmlField);
                                }
                                catch (Exception ex)
                                {
                                    PortalLog.LogString("LinkChecker: Error checking link '{0}' at '{1}'", Url, PageUri.ToString());
                                }
                            }
                        }
                        break;
                    case "URL":
                        try
                        {
                            if ((Item[field.Title] != null) && !string.IsNullOrEmpty(Item[field.Title].ToString()))
                            {
                                SPFieldUrlValue UrlValue = new SPFieldUrlValue(Item[field.Title].ToString());
                                try
                                {
                                    ValidateLink(PageUri, new Uri(UrlValue.Url), BrokenLinkType.LinkField);
                                }
                                catch (Exception ex)
                                {
                                    PortalLog.LogString("LinkChecker: Error checking link '{0}' at '{1}'", UrlValue.Url, PageUri.ToString());
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            PortalLog.LogString("LinkChecker: Error: Unable to check links for field {0} at {1} - Error Message: {2}, Stack Trace: {3}", field.Title, Item.Url, ex.Message, ex.StackTrace);
                        }
                        break;
                    case "SummaryLinks":
                        try
                        {
                            if ((Item[field.Title] != null) && !string.IsNullOrEmpty(Item[field.Title].ToString()))
                            {
                                SummaryLinkFieldValue LinksFieldValue = new SummaryLinkFieldValue(Item[field.Title].ToString());

                                foreach (SummaryLink Link in LinksFieldValue.SummaryLinks)
                                {
                                    try
                                    {
                                        ValidateLink(PageUri, new Uri(Link.LinkUrl), BrokenLinkType.SummaryLinkField);
                                    }
                                    catch (Exception ex)
                                    {
                                        PortalLog.LogString("LinkChecker: Error checking link '{0}' at '{1}'", Link.LinkUrl, PageUri.ToString());
                                    }
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            PortalLog.LogString("LinkChecker: Error: Unable to check links for field {0} at {1} - Error Message: {2}, Stack Trace: {3}", field.Title, Item.Url, ex.Message, ex.StackTrace);
                        }
                        break;
                }
            }
        }

        /// <summary>
        /// Scans through supported web parts on the specific page to check to see if there are any broken links in them
        /// </summary>
        /// <param name="File">The file to parse the web parts from</param>
        /// <param name="PageUri">The full url of the page</param>
        private void ScanWebParts(SPFile File, Uri PageUri)
        {
            SPLimitedWebPartManager wpm = File.GetLimitedWebPartManager(System.Web.UI.WebControls.WebParts.PersonalizationScope.Shared);
            StringCollection AllLinks;

            foreach (WebPart wp in wpm.WebParts)
            {
                switch (wp.GetType().FullName)
                {
                    case "Microsoft.SharePoint.WebPartPages.ContentEditorWebPart":
                        ContentEditorWebPart ContentEditorWP = (ContentEditorWebPart)wp;

                        AllLinks = LinkCheckerUtilities.GetLinksFromHTML(ContentEditorWP.Content.OuterXml);

                        foreach (string Url in AllLinks)
                        {
                            ValidateLink(PageUri, new Uri(PageUri, Url), BrokenLinkType.ContentEditorWebPart);
                        }
                        break;
                    case "Microsoft.SharePoint.Publishing.WebControls.SummaryLinkWebPart":
                        SummaryLinkWebPart SummaryLinkWP = (SummaryLinkWebPart)wp;

                        AllLinks = LinkCheckerUtilities.GetLinksFromHTML(SummaryLinkWP.SummaryLinkStore);

                        foreach (string Url in AllLinks)
                        {
                            ValidateLink(PageUri, new Uri(PageUri, Url), BrokenLinkType.SummaryLinkWebPart);
                        }
                        break;
                }
            }

            wpm.Dispose();

        }

        /// <summary>
        /// Adds a new broken link entry to the current objects collection
        /// </summary>
        /// <param name="Source">Where the broken link came from</param>
        /// <param name="Details">Information about the broken link</param>
        private void AddBrokenLink(Uri Source, BrokenLink Details)
        {

            if (!CurrentUriHasBrokenLinks)
            {
                BrokenLinkTempFile.WriteStartElement("Page");
                BrokenLinkTempFile.WriteAttributeString("uri", CurrentUri.ToString());
                CurrentUriHasBrokenLinks = true;
            }

            BrokenLinkTempFile.WriteStartElement("BrokenLink");
            BrokenLinkTempFile.WriteAttributeString("link", Details.Url.ToString());
            BrokenLinkTempFile.WriteAttributeString("type", Details.LinkType.ToString());
            BrokenLinkTempFile.WriteEndElement();
        }

        /// <summary>
        /// Validates a link by checking the cache to see if the response has already been checked and recording broken links
        /// </summary>
        /// <param name="PageUri">The Uri of the page the link is on</param>
        /// <param name="UriToValidate">The Uri of the link to check</param>
        /// <param name="LinkType">The type of the link being checked</param>
        private void ValidateLink(Uri PageUri, Uri UriToValidate, BrokenLinkType LinkType)
        {
            if (!CheckedLinks.Contains(UriToValidate.ToString()))
            {
                if (!LinkCheckerUtilities.LinkIsValid(UriToValidate))
                {
                    AddBrokenLink(PageUri, new BrokenLink(UriToValidate, LinkType));
                    CheckedLinks.Add(UriToValidate.ToString(), false);
                }
                else
                {
                    CheckedLinks.Add(UriToValidate.ToString(), true);
                }
            }
            else
            {
                if ((bool)CheckedLinks[UriToValidate.ToString()] == false)
                {
                    AddBrokenLink(PageUri, new BrokenLink(UriToValidate, LinkType));
                }
            }
        }




        private void StartNewUri(Uri PageBeingScanned)
        {
            CurrentUri = PageBeingScanned;
            CurrentUriHasBrokenLinks = false;
        }


        private void FinishCurrentUri()
        {
            if (CurrentUriHasBrokenLinks)
            {
                BrokenLinkTempFile.WriteEndElement();
                BrokenLinkTempFile.Flush();
            }
        }
    }
}
